import numpy as np
from scipy.signal import convolve2d
import sys


"""
I tried to have a little prompt when the user presses ^C (keyboard interrupt)
But that failed for the following reasons:
1. If i just have a listener that catches KeyboardInterrupts and asks the user if he wants to continue, as the main program is already listening for input, the listener CANNOT READ FROM STANDARD INPUT (and thus cannot read the user's response whether to continue.)
2. If i simply catch KeyboardInterrupts, it doesn't know where to continue from. (therefore i have to try-catch every block which is extremely annoying)
3. If i use multithreading, Windows is too stupid to have a thread.pause() functionality.
So I simply 'try-except'ed every place where the program listens for input. I'm still working on how to listen for Ctrl+C for the time the program is not waiting for input
"""


def askContinue() -> bool:
    """
    Asks the user if he wants to continue the game. To be called when a KeyboardInterrupt is detected.

    Returns
    -------
    bool
        The user's response, True means the user wants to continue.
    """
    try:
        while True:
            s = input("\nDo you want to continue? (Y/N)\n")
            if s.lower() == 'n' or s.lower() == 'no':
                return False
            if s.lower() == 'y' or s.lower() == 'yes':
                return True
            print("Invalid Input.")
    except KeyboardInterrupt:
        print("Exiting now...")
        sys.exit(0)
    except (ValueError, TypeError):
        print("Invalid, try again.")
        return askContinue()


def getSize():
    """
    Asks the user to input the size of the board.

    Returns
    -------
    tuple(int, int)
        The size of the board. No of cols and No of rows respectively.
    """
    while True:
        try:
            print("Plz input the size of the board, leave blank for default")
            s = input("Input number of cols & rows (both > 4): ")
            if s.replace(' ', '') == '':
                print("No input detected. Using default settings.")
                return 7, 6
            s = s.replace(',', ' ').split()
            while('' in s) :
                s.remove('')
            its = list(map(int, s))
            if len(its) == 2 and its[0] > 4 and its[1] > 4:
                return its[0], its[1]
            print("Invalid. Try again.")
            print()
        except (ValueError, TypeError):
            print("Invalid. Try again.")
            print()
        except KeyboardInterrupt:
            res = askContinue()
            if res:
                continue
            else:
                print("Exiting...")
                sys.exit(0)


"""
These are the covolution win-detection filter masks (aka kernels).
used in winningMove()
"""
horizontal_kernel = np.array([[1,1,1,1]])
vertical_kernel = np.transpose(horizontal_kernel)
diag1_kernel = np.eye(4, dtype=np.uint8)
diag2_kernel = np.fliplr(diag1_kernel)
detection_kernels = [horizontal_kernel, vertical_kernel, diag1_kernel, diag2_kernel]



def winningMove(board, player) -> bool:
    """
    tests if the player has already won.

    Parameters
    ----------
    board : 2d np array

    player : int, the player to be checked

    Returns
    -------
    bool
        whether the player had won
    """
    # ADVANCED MATH THAT DOES MATHS AND IS VERY FAST AND GOOD
    for kernel in detection_kernels:
        if (convolve2d(board == player, kernel, mode="valid") == 4).any():
            return True
    return False


def getPosition(board, col) -> int:
    """
    Gets the first empty position in the specified column of the board.
    i.e. the row index if a piece is put on the column on the board.

    Parameters
    ----------
    board : Numpy2DArray
        The current board state.
    col : int
        The column which the piece is being put in.

    Returns
    -------
    int
        The row index of the resultant position.
    """
    if col < 0 or col >= board.shape[1]:
        return -1
    for p in range(board.shape[0]):
        if board[p][col] == 0:
            return p
    return -1


def getCol(board):
    """
    Get user input on which column the player wants to play.

    Parameters
    ----------
    board : np 2d array
        The current board state.

    Returns
    -------
    int
        the column the player requested.
    """
    while True:
        try:
            col = int(input("Which column do u want to play? ")) - 1
            while col < 0 or col >= board.shape[1]:
                print("Invalid. Try again")
                col = int(input("Which column do u want to play? ")) - 1
            return col
        except ValueError:
            print("Invalid. Try again")


def move(board, player):
    """
    A player making a move. Asks for input in stdin.

    Parameters
    ----------
    board : np 2d array
        The current board state.
    player : int
        the player making a move.

    Returns
    -------
    np 2d array
        The altered board state after the player has made the move.
    """
    # old_board = board
    col = getCol(board)
    row = getPosition(board, col)
    while row == -1:
        print("column already full")
        col = getCol(board)
        row = getPosition(board, col)
    board[row][col] = player
    return board


charmap = {
    0: ' ',
    1: 'o',
    2: 'x'
}


def display(board, player):
    """
    Displays the current board and whose turn it is.

    Parameters
    ----------
    board : np 2d array
        The current board state.
    player : int
        The player to be displayed
    """
    print(' ', end='')
    for x in range(board.shape[1]):
        print("%3d"%(x+1), end='')
    print()
    for x in range(board.shape[0]-1, -1, -1):
        print(x+1, end = '')
        for y in range(board.shape[1]):
            print('  '+charmap[board[x][y]], sep = '', end='') # yes this is hardcoded but i can't be bothered
        print()
    print("Player %d\'s turn now, his symbol is: %s"%(player, charmap[player]))


def main_game():
    """
    Runs one game. Does not give prompt to ask if to start new game.
    """
    col, row = getSize()
    board = np.zeros((row, col), dtype=np.uint8)
    cur_player = 1
    for _ in range(col*row):
        display(board, cur_player)
        board = move(board, cur_player)
        if winningMove(board, cur_player):
            display(board, cur_player)
            print('Player %d Wins! His symbol was %s'%(cur_player, 'o' if cur_player == 1 else 'x'))
            break
        cur_player = 3 - cur_player
        print()
        print('===============================')
    else:
        print("DRAW")


def game():
    """
    Calls main_game() and gives prompts to ask if to start new game.
    """
    while True:
        main_game()
        isOver = False
        while True:
            s = input("Do you want to start a new game? (Y/N)\n")
            if s.lower() == 'n' or s.lower() == 'no':
                isOver = True
                break
            if s.lower() == 'y' or s.lower() == 'yes':
                break
            print("Invalid Input.")
        if isOver:
            break
    print("Exiting...")


if __name__ == '__main__':
    game()
